package org.smartboot.compare.comparator;

import org.smartboot.compare.ComparatorContext;
import org.smartboot.compare.ComparatorRegister;
import org.smartboot.compare.FieldCache;
import org.smartboot.compare.GlobalConfiguration;
import org.smartboot.compare.NameType;
import org.smartboot.compare.Option;
import org.smartboot.compare.Path;
import org.smartboot.compare.difference.Difference;
import org.smartboot.compare.difference.DifferenceError;
import org.smartboot.compare.difference.DifferenceGroup;
import org.smartboot.compare.difference.BaseDifference;
import org.smartboot.compare.difference.TypeDifference;
import org.smartboot.compare.utils.InternalClassUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;

/**
 * @author qinluo
 * @version 1.0.0
 * @since 2019-05-27 17:48
 * cs:off
 */
public class DispatcherComparator extends AbstractComparator<Object> {

    @Override
    @SuppressWarnings({"rawtypes", "unchecked"})
    public Difference compare(Object expect, Object actual, ComparatorContext<Object> context) {
        Class<?> expectType = expect != null ? expect.getClass() : null;
        Class<?> actualType = actual != null ? actual.getClass() : null;

        // Expect and actual are different typed object.
        if (expectType != null && actualType != null
                && (InternalClassUtils.lookupHighestType(expectType) != InternalClassUtils.lookupHighestType(actualType))) {
            return new TypeDifference(context.getPath(), expect, actual);
        }

        // strict mode : Expect and actual are same type, such as List, but actually type is not same.
        if (expectType != null && actualType != null && expectType != actualType && !context.hasOption(Option.LOOSE_MODE)) {
            return new TypeDifference(context.getPath(), expect, actual);
        }

        // Expect and actual have same 'type', or one of null.
        Class<?> nonnullType = expectType != null ? expectType : actualType;
        // Find any comparator.
        Comparator<?> comparator = ComparatorRegister.findComparator(nonnullType);
        ComparatorContext newContext = context.clone(expect, actual);

        if (nonnullType.isArray()) {
            if (comparator == null || comparator instanceof DispatcherComparator) {
                context.addMessage("Cannot find comparator for array-type " + nonnullType.getName());
            } else {
                return comparator.compare(newContext);
            }
            return Difference.SAME;
        }

        if (!InternalClassUtils.isSimple(nonnullType)
                && checkRecycled(context, expect, actual, true)) {
            context.addMessage("detect recycle reference in path " + context.getPath());
            context.incrRecycle();
            return Difference.SAME;
        }

        // check has actually comparer
        if (comparator != null && !(comparator instanceof DispatcherComparator)) {
            return comparator.compare(newContext);
        }

        if (!InternalClassUtils.isCollection(nonnullType)) {
            try {
                //如果相应对象自己定义了equals方法，则使用equals方法进行比较
                Method em = nonnullType.getMethod("equals", Object.class);
                if (!context.hasOption(Option.DISABLE_EQUALS) && em.getDeclaringClass() == nonnullType) {
                    context.addMessage("Use customized equals method at " + context.getPath());
                    if (!Objects.equals(expect, actual)) {
                        return new BaseDifference(context.getPath(), expect, actual);
                    }
                    return Difference.SAME;
                }
            } catch (Exception e) {
                return new DifferenceError(context.getPath(), e);
            } catch (StackOverflowError error) {
                // avoid stack overflow in equals method.
                throw new RuntimeException("Please specified DISABLE_EQUALS in equals method.");
            }
        }

        // Use default behavior.
        if (GlobalConfiguration.isUseDefaultEqualsBehavior(nonnullType, context)) {
            if (!Objects.equals(expect, actual)) {
                return new BaseDifference(context.getPath(), expect, actual);
            }
            return Difference.SAME;
        }

        List<FieldCache> fields = InternalClassUtils.getAllFields(nonnullType);
        if (fields.isEmpty()) {
            return Difference.SAME;
        }

        DifferenceGroup group = DifferenceGroup.of();

        for (FieldCache f : fields) {
            NameType nameType = f.getNamedType();
            Field field = f.getField();
            Path newPath = context.createFieldPath(field);
            if (context.getFilters().filtered(f, context, newPath)) {
                context.addSkippedField(newPath.getFullPath());
                continue;
            }

            Object ev = null;
            Object av = null;

            Class fieldType = field.getType();
            try {
                ev = field.get(expect);
                av = field.get(actual);
            } catch (Exception e) {
                //do nothing
            }
            // check equals in references.
            if (ev == av) {
                continue;
            }

            if (!InternalClassUtils.isSimple(fieldType)
                    && checkRecycled(context, ev, av, false)) {
                context.addMessage("detect recycle reference in path " + newPath.getFullPath());
                context.incrRecycle();
                continue;
            }

            ComparatorContext<Object> nc = context.clone(ev, av);
            nc.incr();
            nc.setPath(newPath);

            Comparator compare = ComparatorRegister.findComparator(nameType);
            if (compare != null) {
                group.addDifference(compare.compare(nc));
            } else {
                group.addDifference(ComparatorRegister.findComparator(fieldType).compare(nc));
            }

            // 立即中断
            if (group.hasDifferences() && context.hasOption(Option.IMMEDIATELY_INTERRUPT)) {
                break;
            }
        }

        return group;
    }
}
